/**
 *  @file IPCSocket.cpp
 */
#include "IPCSocket.h"
#include "../common/CppMemPool.h"
#include "../common/ObjPtrT.h"
#include "../common/CppflibMain.h"
#include "../util/Random.h"
#include "SocketException.h"
#if defined(WIN32)
#   include <windows.h>
#elif defined(linux)
#   include "../os/FileSys.h"
#   include <sys/types.h>
#   include <sys/socket.h>
#   include <sys/select.h>
#   include <sys/un.h>
#   include <sys/ioctl.h>
#   include <unistd.h>
#   include <fcntl.h>
#   include <errno.h>
#   include <stdio.h>
#endif

using namespace cppflib::util;

namespace cppflib
{

namespace net
{

/**
 *  Throw exception if it isn't a server socket
 */
PRIVATE void __CheckServerContext(bool isServer)
{
   if (!isServer) 
      throw SocketException(_S("This isn't a server socket"));
}

/**
 *  Throw exception if it isn't a client socket
 */
PRIVATE void __CheckClientContext(bool isServer)
{
   if (isServer) 
      throw SocketException(_S("This isn't a client socket"));
}

/**
 *  Create a random name
 */
PRIVATE const CFString __CreateRandomName()
{
   CFString randomName;
   Random * pRan = new Random();
   pRan->GetString(randomName, Random::RT_ALPHANUMERIC, 32);
   cppmempool::Release(pRan);
   return randomName;
}


#if defined(WIN32)
/**
 *  On Windows, a pipe has name pattern like this
 *  "\\.\pipe\XXXXXXX"
 */
PRIVATE pcwstr_t SOCKET_PREFIX = _S("\\\\.\\pipe\\");
#elif defined(linux)
/**
 *  On Linux, the socket file is usually created at /tmp
 */
PRIVATE pcwstr_t SOCKET_PREFIX = _S("/tmp/");
#endif

/**
 *  Create a socket name
 */
PRIVATE CFString * __CreateSocketName(const CFString &socketName)
{
    CFString *pSocketName = new CFString();
    pSocketName->Format(_S("%s%s"), SOCKET_PREFIX, static_cast<pcwstr_t>(socketName));
    cppmempool::Retain(pSocketName); // retain
    return pSocketName;
}

#if defined(WIN32)

/**
 *  Definition of IPC context on Windows.
 *  Internally, this IPCSocket is implemented by NamedPipe on Windows
 */
struct _ipcsockcontext
{
   bool isServer;
   CFString *pSocketName;
   HANDLE hPipe;
   OVERLAPPED *pOverlapped;
};

/**
 *  Create OVERLAPPED structure to implement the 'timeout' feature for 'Accept' and 'Receive'
 */
PRIVATE OVERLAPPED * __CreateOverlapped()
{
   OVERLAPPED * pOverlapped = static_cast<OVERLAPPED*>(::MRP_Calloc(1, sizeof(OVERLAPPED)));
   pOverlapped->hEvent = ::CreateEvent(NULL, TRUE, FALSE, NULL); // create a manual reset event
   return pOverlapped;
}

/**
 *  Delete OVERLAPPED structure
 */
PRIVATE void __DeleteOverlapped(OVERLAPPED * pOverlapped)
{
   if (pOverlapped->hEvent)
       ::CloseHandle(pOverlapped->hEvent);
   cppmempool::Release(pOverlapped);
}

/**
 *  This is to construct a client socket
 */
IPCSocket::IPCSocket(void)
{
   this->pContext = static_cast<ipcsockcontext_t*>(::MRP_Calloc(1, sizeof(ipcsockcontext_t)));
   this->pContext->isServer = false;
   this->pContext->hPipe = NULL;
   this->pContext->pOverlapped = __CreateOverlapped();
   this->pContext->pSocketName = NULL;
}

/**
 *  This is to construct a server socket
 *  
 *  @param [in] socketName -- an arbitrary name for the socket (best to use alphanumerics only)
 *                            Pass an empty string if you want this class to generate a random name for you
 */
IPCSocket::IPCSocket(const CFString &socketName)
{
   this->pContext = static_cast<ipcsockcontext_t*>(::MRP_Calloc(1, sizeof(ipcsockcontext_t)));
   this->pContext->isServer = true;
   this->pContext->hPipe = NULL;
   this->pContext->pOverlapped = __CreateOverlapped();
   this->pContext->pSocketName = socketName.IsEmpty() ? __CreateSocketName(__CreateRandomName()) :  __CreateSocketName(socketName);
}

IPCSocket::~IPCSocket(void)
{
   if (pContext->isServer)
      DisconnectClient();
   Close();

   // delete the context
   __DeleteOverlapped(pContext->pOverlapped);
   if (pContext->pSocketName) 
      cppmempool::Release(pContext->pSocketName);
   cppmempool::Release(pContext);
}

/**
 *  For server socket only.
 *  Create a socket ready for listening
 */
void IPCSocket::Create()
{
   __CheckServerContext(pContext->isServer);
   if (pContext->hPipe)
      throw SocketException(_S("Socket already created"));

   const u32_t BUFSIZE = 1024;
   const u32_t DEF_CONNTIMEOUT = 2000;
   HANDLE hPipe = ::CreateNamedPipe(static_cast<pcwstr_t>(*pContext->pSocketName),
		                             PIPE_ACCESS_DUPLEX | WRITE_DAC | FILE_FLAG_OVERLAPPED,
									 PIPE_TYPE_BYTE | PIPE_READMODE_BYTE | PIPE_WAIT,
									 1, BUFSIZE, BUFSIZE, DEF_CONNTIMEOUT, NULL);

   if (hPipe == INVALID_HANDLE_VALUE)
      throw SocketException(_S("Failed to create server socket"));

   pContext->hPipe = hPipe;
}

/**
 *  For server socket only.
 *  Wait for a client socket to connect
 *
 *  @param [in] timeout -- timeout in milliseconds
 *
 *  @return true -- client connected
 *  @return false -- timeout without client connected
 */
bool IPCSocket::Accept(int timeout)
{
   __CheckServerContext(pContext->isServer);

   if (::ConnectNamedPipe(pContext->hPipe, pContext->pOverlapped))
      return true;

   u32_t err = ::GetLastError();
   if (err == ERROR_PIPE_CONNECTED) { // a client is connected before 'ConnectNamedPipe' is called
      return true;
   }
   else if (err == ERROR_IO_PENDING) {
      u32_t ret = ::WaitForSingleObject(pContext->pOverlapped->hEvent, timeout);
      if (ret == WAIT_OBJECT_0) 
         return true;
      
      if (ret != WAIT_TIMEOUT)
         throw SocketException(_S("Failed to wait for a client to connect"));
   }

   return false; // timeout
}

/**
 *  For client socket only.
 *  Connect to server before any data transfer.
 *
 *  @param [in] socketName -- target server socket name (alphanumeric only)
 *  @param [in] timeout -- connect timeout
 *
 *  @return true -- connected
 *  @return false -- timeout
 */
bool IPCSocket::Connect(const CFString &socketName, int timeout)
{
   __CheckClientContext(pContext->isServer);
   if (pContext->hPipe)
      throw SocketException(_S("Socket not closed yet"));

   if (pContext->pSocketName) {
      cppmempool::Release(pContext->pSocketName);
      pContext->pSocketName = NULL;
   }
   pContext->pSocketName = __CreateSocketName(socketName);

   // wait to see if server is ready to connect
   if (!::WaitNamedPipe(static_cast<pcwstr_t>(*pContext->pSocketName), timeout)) {
      if (::GetLastError() == ERROR_SEM_TIMEOUT)
         return false;
      throw SocketException(_S("Failed to wait for server availability"));
   }

   // open the existing named pipe
   HANDLE hPipe = ::CreateFile(static_cast<pcwstr_t>(*pContext->pSocketName),
                               GENERIC_READ | GENERIC_WRITE,
                               0, 
                               NULL,
                               OPEN_EXISTING,
                               FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED,
                               NULL);
   if (hPipe == INVALID_HANDLE_VALUE) 
      throw SocketException(_S("Failed to connect to server"));
   
   pContext->hPipe = hPipe;
   return true;
}

/**
 *  Receive data from peer
 *
 *  @param [in] timeout -- timeout in milliseconds
 *  @param [in] pBuf -- pointer to buffer to receive data
 *  @param [in] bufLen -- length of buffer
 *
 *  @return number of bytes received
 *  @return 0 -- timeout 
 */
int IPCSocket::Receive(int timeout, u8_t * pBuf, int bufLen)
{
   int bytesRead = 0;
   if (::ReadFile(pContext->hPipe, pBuf, bufLen, reinterpret_cast<LPDWORD>(&bytesRead), pContext->pOverlapped)) 
       return bytesRead;

   u32_t err = ::GetLastError();
   if (err == ERROR_IO_PENDING) {
       u32_t ret = ::WaitForSingleObject(pContext->pOverlapped->hEvent, timeout);
       if (ret == WAIT_OBJECT_0) {
          ::GetOverlappedResult(pContext->hPipe, pContext->pOverlapped, reinterpret_cast<LPDWORD>(&bytesRead), FALSE);
          return bytesRead;
       }

       if (ret != WAIT_TIMEOUT)
          throw SocketException(_S("Failed to receive data from peer"));
   }

   return 0; // timeout
}

/**
 *  Send data to peer
 *
 *  @param [in] pBuf -- pointer to buffer which contains data to be sent
 *  @param [in] bufLen -- length of buffer
 *
 *  @return number of bytes sent
 */
int IPCSocket::Send(const u8_t * pBuf, int bufLen)
{
   int bytesSent = 0;
   ::WriteFile(pContext->hPipe, pBuf, bufLen, reinterpret_cast<LPDWORD>(&bytesSent), pContext->pOverlapped);
   return bytesSent;
}

/**
 *  For server socket only
 *  Close the connection from client
 */
void IPCSocket::DisconnectClient()
{
   __CheckServerContext(pContext->isServer);

   if (pContext->hPipe)
      ::DisconnectNamedPipe(pContext->hPipe);
}

/**
 *  Close the socket
 */
void IPCSocket::Close()
{
   if (pContext->hPipe) {
      ::CloseHandle(pContext->hPipe);
      pContext->hPipe = NULL;
   }
}



#elif defined(linux)

#define INVALID_SOCKET ((ptr_t)(~0))
#define SOCKET_ERROR           (-1)

/**
 *  Definition of IPC context on Linux.
 *  Internally, this IPCSocket is implemented by Unix Domain socket on Linux
 */
struct _ipcsockcontext
{
   bool isServer;
   CFString *pSocketName;
   ptr_t sock;       ///< socket handle
   ptr_t peerSock;   ///< peer socket handle for server only
};

/**
 *  Assign a socket name to the context
 */
PRIVATE void __AssignSocketName(ipcsockcontext_t *pContext, const CFString &socketName)
{
   if (pContext->isServer && socketName.IsEmpty()) {
      // only server socket can make a random name
      pContext->pSocketName = __CreateSocketName(__CreateRandomName());
   }
   else if (socketName.StartsWith(_S('/'))) {
      pContext->pSocketName = new CFString(socketName);
      cppmempool::Retain(pContext->pSocketName);
   }
   else {
      pContext->pSocketName = __CreateSocketName(socketName);
   }
}


/**
 *  This is to construct a client socket
 */
IPCSocket::IPCSocket(void)
{
   this->pContext = static_cast<ipcsockcontext_t*>(::MRP_Calloc(1, sizeof(ipcsockcontext_t)));
   this->pContext->isServer = false;
   this->pContext->pSocketName = NULL;
   this->pContext->sock = INVALID_SOCKET;
   this->pContext->peerSock = INVALID_SOCKET;
}

/**
 *  This is to construct a server socket
 *  
 *  @param [in] socketName -- an arbitrary name for the socket (best to use alphanumerics only)
 *                            Pass an empty string if you want this class to generate a random name for you
 *
 *                            For Linux only, if the name begins with a '/', it is assumed the caller wants
 *                            to place the socket file at his/her own wish, otherwise, it will be placed in
 *                            a default directory
 */
IPCSocket::IPCSocket(const CFString &socketName)
{
   this->pContext = static_cast<ipcsockcontext_t*>(::MRP_Calloc(1, sizeof(ipcsockcontext_t)));
   this->pContext->isServer = true;
   __AssignSocketName(this->pContext, socketName);
   this->pContext->sock = INVALID_SOCKET;
   this->pContext->peerSock = INVALID_SOCKET;
}

IPCSocket::~IPCSocket(void)
{
   if (pContext->isServer)
      DisconnectClient();
   Close();

   // delete the context
   if (pContext->pSocketName) 
      cppmempool::Release(pContext->pSocketName);
   cppmempool::Release(pContext);
}

/**
 *  For server socket only.
 *  Create a socket ready for listening
 */
void IPCSocket::Create()
{
   __CheckServerContext(pContext->isServer);
   if (pContext->sock != INVALID_SOCKET)
      throw SocketException(_S("Socket already created"));

   pContext->sock = socket(AF_UNIX, SOCK_STREAM, 0);
   if (pContext->sock == INVALID_SOCKET) 
      throw SocketException(_S("Create socket failed"));
      
   
   // bind the socket
   ObjPtrT<char> sockName(pContext->pSocketName->ToUtf8String());
   struct sockaddr_un server;
   server.sun_family = AF_UNIX;
   strcpy(server.sun_path, sockName);
   if (bind(pContext->sock, (struct sockaddr*)&server, sizeof(struct sockaddr_un)) != 0) 
      throw SocketException(_S("Bind socket failed"));
	  
   // start listening
   listen(pContext->sock, 1);
}

/**
 *  Check if socket is ready to read
 *
 *  @return true -- socket is ready for reading
 *  @return false -- timeout
 */
PRIVATE bool __IsSocketPending(ptr_t sock, int timeout)
{
    fd_set readFd;
	FD_ZERO(&readFd);
	FD_SET(sock, &readFd);
	
	ptr_t maxSocket = sock;  // max socket number
    
	struct timeval timeOutStruct;
	timeOutStruct.tv_sec = timeout / 1000;
	timeOutStruct.tv_usec = (timeout % 1000) * 1000;

    // wait for pending data
    int ret =  ::select((int)(maxSocket + 1), &readFd, NULL, NULL, &timeOutStruct);
    if (ret < 0)
       throw SocketException(_S("Check socket pending failed"));

    return (ret > 0);
}


/**
 *  For server socket only.
 *  Wait for a client socket to connect
 *
 *  @param [in] timeout -- timeout in milliseconds
 *
 *  @return true -- client connected
 *  @return false -- timeout without client connected
 */
bool IPCSocket::Accept(int timeout)
{
   __CheckServerContext(pContext->isServer);

   if (pContext->peerSock != INVALID_SOCKET)
      throw SocketException(_S("Socket is still connecting with a client"));

   if (!__IsSocketPending(pContext->sock, timeout))
      return false;

   pContext->peerSock = accept(pContext->sock, 0, 0);
   return true;
}

/**
 *  Check if socket is ready to write
 *
 *  @return true -- socket is ready for writing
 *  @return false -- timeout
 */
PRIVATE bool __IsSocketReadyToWrite(ptr_t sock, int timeout)
{
    fd_set writeFd;
	FD_ZERO(&writeFd);
	FD_SET(sock, &writeFd);
	
	ptr_t maxSocket = sock;  // max socket number
    
	struct timeval timeOutStruct;
	timeOutStruct.tv_sec = timeout / 1000;
	timeOutStruct.tv_usec = (timeout % 1000) * 1000;

    // wait for ready to write data
    int ret =  ::select((int)(maxSocket + 1), NULL, &writeFd, NULL, &timeOutStruct);
    if (ret < 0)
       throw SocketException(_S("Check socket for writing failed"));

    return (ret > 0);
}

/**
 *  class to toggle blocking and non-blocking state
 */
class ToggleBlockingState
{
private:
   ptr_t sock;
public:
   ToggleBlockingState(ptr_t sock)
   {
      this->sock = sock;
      SetBlocking(false); // set to non-blocking
   }
       
   ~ToggleBlockingState()
   {
      SetBlocking(true);  // restore finally
   }

private:
   void SetBlocking(bool isEnableBlock)
   {
      int orgFlags = ::fcntl(sock, F_GETFL);
      orgFlags = isEnableBlock ? (orgFlags & ~O_NONBLOCK) : (orgFlags | O_NONBLOCK);
      ::fcntl(sock, F_SETFL, orgFlags);
   }
};

/**
 *  Try to connect with the non-blocking socket
 */
PRIVATE bool __TryConnect(ipcsockcontext_t *pContext)
{
   // set up connect stuff
   ObjPtrT<char> sockName(pContext->pSocketName->ToUtf8String());
   struct sockaddr_un server;
   server.sun_family = AF_UNIX;
   strcpy(server.sun_path, sockName);

   int err = ::connect(pContext->sock, (struct sockaddr *) &server, sizeof(struct sockaddr_un));
   if (err != SOCKET_ERROR) {
      return true;
   }
   else {
      if (cppflib::GetLastError() == EISCONN)   // already connected
         return true;
   }

   if (cppflib::GetLastError() != EINPROGRESS) 
      throw SocketException(_S("try connect failed"));

   return false;
}


/**
 *  For client socket only.
 *  Connect to server before any data transfer.
 *
 *  @param [in] socketName -- target server socket name (alphanumeric only)
 *  @param [in] timeout -- connect timeout
 *
 *  @return true -- connected
 *  @return false -- timeout
 */
bool IPCSocket::Connect(const CFString &socketName, int timeout)
{
   __CheckClientContext(pContext->isServer);
   if (pContext->sock != INVALID_SOCKET)
      throw SocketException(_S("Socket not closed yet"));

   if (pContext->pSocketName) {
      cppmempool::Release(pContext->pSocketName);
      pContext->pSocketName = NULL;
   }

   // assign target socket name first
   __AssignSocketName(this->pContext, socketName);
   
   // create socket
   pContext->sock = socket(AF_UNIX, SOCK_STREAM, 0);
   if (pContext->sock == INVALID_SOCKET) 
      throw SocketException(_S("Create socket failed"));
      
   // set to non-blocking first
   ToggleBlockingState toggleBlocking(pContext->sock);

   if (__TryConnect(pContext))
      return true;

   if (!__IsSocketReadyToWrite(pContext->sock, timeout)) 
      return false;

   // Linux require to further read 'SO_ERROR' to confirm whether 'connect' succeeds or not
   int soError = -1;
   socklen_t optLen = sizeof(int);
   if (::getsockopt(pContext->sock, SOL_SOCKET, SO_ERROR, &soError, &optLen) == SOCKET_ERROR)
      throw SocketException(_S("getsockopt during connect failed"));

   if (soError != 0)
      throw SocketException(_S("Connect finally failed"));
   
   return true;
}

PRIVATE ptr_t __GetSock(ipcsockcontext_t *pContext)
{
   return pContext->isServer ? pContext->peerSock : pContext->sock;
}

/**
 *  @return > 0 -- number of bytes received
 *  @return 0 -- timeout
 */
int IPCSocket::Receive(int timeout, u8_t * pBuf, int bufLen)
{
   if (__IsSocketPending(__GetSock(pContext), timeout))
      return ::recv(__GetSock(pContext), reinterpret_cast<char*>(pBuf), bufLen, 0);
   return 0;
}

/**
 *  @return number of bytes sent
 */
int IPCSocket::Send(const u8_t * pBuf, int bufLen)
{
   return ::send(__GetSock(pContext), reinterpret_cast<const char*>(pBuf), bufLen, 0);
}

/**
 *  For server socket only
 *  Close the connection from client
 */
void IPCSocket::DisconnectClient()
{
   __CheckServerContext(pContext->isServer);

   if (pContext->peerSock != INVALID_SOCKET) {
      ::close(pContext->peerSock);
      pContext->peerSock = INVALID_SOCKET;
   }
}

/**
 *  Close socket
 */
void IPCSocket::Close()
{
   if (pContext->sock != INVALID_SOCKET) {
      ::close(pContext->sock);
      pContext->sock = INVALID_SOCKET;

      // remove the socket file, too
      if (pContext->isServer && pContext->pSocketName) {
         os::DelFile(static_cast<pcwstr_t>(*pContext->pSocketName));
      }
   }
   
}


#endif

/**
 *  @return true -- it is a server socket
 */
bool IPCSocket::IsServer()
{
   return pContext->isServer;
}

/**
 *  @return name of bound/connected socket without decoration
 */
const CFString IPCSocket::GetSocketName()
{
   if (pContext->pSocketName) {
      sz_t nameStart = CFString::WStrlen(SOCKET_PREFIX);
      return pContext->pSocketName->Mid(static_cast<int>(nameStart));
   }

   return CFString::Empty();
}

/**
 *  @return final name of bound/connected socket
 */
const CFString IPCSocket::GetFullSocketName()
{
   return (pContext->pSocketName) ? *pContext->pSocketName : CFString::Empty();
}

} // end of namespace net

} // end of namespace cppflib

